package sketch.specs;

import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;

import randoop.AbstractGenerator;
import randoop.BugInRandoopException;
import randoop.Check;
import randoop.ComponentManager;
import randoop.ContractCheckingVisitor;
import randoop.DefaultTestFilter;
import randoop.EqualsHashcode;
import randoop.EqualsReflexive;
import randoop.EqualsSymmetric;
import randoop.EqualsToNullRetFalse;
import randoop.ExecutableSequence;
import randoop.ExecutionVisitor;
import randoop.ExpectedExceptionCheck;
import randoop.ForwardGenerator;
import randoop.ITestFilter;
import randoop.ObjectContract;
import randoop.RConstructor;
import randoop.RandoopListenerManager;
import randoop.SeedSequences;
import randoop.Sequence;
import randoop.StatementKind;
import randoop.Variable;
import randoop.main.GenInputsAbstract;
import randoop.util.Reflection;
import sketch.main.SketchTestOptions;
import sketch.util.Checker;
import sketch.util.Randomness;

class RandoopWrapper {
	
	//keep track of all static state
	Collection<Class<?>> classes = new LinkedHashSet<Class<?>>();
	List<StatementKind> model = new LinkedList<StatementKind>();
	
	//all the initial sequence
	final Set<Sequence> components = new LinkedHashSet<Sequence>();
	final ComponentManager componentMgr;
	final RandoopListenerManager listenerMgr = new RandoopListenerManager();
	final LinkedList<ITestFilter> outputTestFilters;
	final int inputlimit = GenInputsAbstract.inputlimit;
	final List<ExecutionVisitor> visitors = new ArrayList<ExecutionVisitor>();
	
	final RConstructor objectConstructor;
	
	//the test generator
	AbstractGenerator explorer = null;
	
	public RandoopWrapper() {
		//add the <init> of object
		try {
		    this.objectConstructor = RConstructor.getRConstructor(Object.class.getConstructor());
		    if (!model.contains(objectConstructor))
		      model.add(objectConstructor);
		 } catch (Exception e) {
		    throw new BugInRandoopException(e); // Should never reach here!
		 }
		 //add some initial seeds
		 components.addAll(SeedSequences.objectsToSeeds(SeedSequences.primitiveSeeds));
		 //the component manager
		 this.componentMgr = new ComponentManager(components);
		 //XXX not class literals yet
		 this.outputTestFilters = new LinkedList<ITestFilter>();
		 outputTestFilters.add(new DefaultTestFilter());
		 //init all visitors
		 initVisitors();
    }
	
	public ComponentManager getComponentManager() {
		return this.componentMgr;
	}
	
	public void addClasses(Collection<Class<?>> classes) {
		this.model.addAll(Reflection.getStatements(classes, null));
	}
	
	private void initVisitors() {
		List<ObjectContract> contracts = new ArrayList<ObjectContract>();
        // Now add all of Randoop's default contracts.
	    contracts.add(new EqualsReflexive());
	    contracts.add(new EqualsToNullRetFalse());
	    contracts.add(new EqualsHashcode());
	    contracts.add(new EqualsSymmetric());
	    ContractCheckingVisitor contractVisitor = new ContractCheckingVisitor(contracts,
	    GenInputsAbstract.offline ? false : true);
	    visitors.add(contractVisitor);
	}
	
	
	List<ExecutableSequence> createAllSequences(float timelimit) {
		Checker.checkTrue(timelimit > 0, "The timelimit: " + timelimit + " should > 0.");
		this.explorer = new ForwardGenerator(model, (int)(timelimit * 1000), inputlimit, componentMgr, null, listenerMgr, outputTestFilters);
		this.explorer.executionVisitor.visitors.addAll(visitors);
		
		//reset the randomize
		Randomness.reset(System.currentTimeMillis());
		
		//random gen sequences
		this.explorer.explore();
		
		List<ExecutableSequence> validSequences = this.remainValidSequences();
		return validSequences;
		
	}
	
	void generateSequences(float timelimit) {
		Checker.checkTrue(timelimit > 0, "The timelimit: " + timelimit + " should > 0.");
		//init the explorer
		this.explorer = new ForwardGenerator(model, (int)(timelimit * 1000), inputlimit, componentMgr, null, listenerMgr, outputTestFilters);
		this.explorer.executionVisitor.visitors.addAll(visitors);
		
		//reset the randomize
		Randomness.reset(System.currentTimeMillis());
		
		//random gen sequences
		this.explorer.explore();
	}
	
	List<ExecutableSequence> createAllSequences(Class<?> clazz, float timelimit) {
		Checker.checkTrue(!classes.contains(clazz), "The clazz: " + clazz + " has not been loaded yet.");
//		Checker.checkTrue(!classes.contains(clazz), "The clazz: " + clazz + " has not been loaded yet.");
//		Checker.checkTrue(timelimit > 0, "The timelimit: " + timelimit + " should > 0.");
//		//init the explorer
//		this.explorer = new ForwardGenerator(model, (int)(timelimit * 1000), inputlimit, componentMgr, null, listenerMgr, outputTestFilters);
//		this.explorer.executionVisitor.visitors.addAll(visitors);
//		
//		//reset the randomize
//		Randomness.reset(System.currentTimeMillis());
//		
//		//random gen sequences
//		this.explorer.explore();
		
		this.generateSequences(timelimit);
		//remove timeout, unrelevant targets, new objects, and exception throwing
		List<ExecutableSequence> results = this.cleanResults(clazz);
		
		return results;
//		List<Sequence> sequences = new LinkedList<Sequence>();
//		for(ExecutableSequence seq : results) {
//			sequences.add(seq.sequence);
//		}
//		return sequences;
	}
	
	ExecutableSequence createSequence(Class<?> clazz, float timelimit) {
//		Checker.checkTrue(!classes.contains(clazz), "The clazz: " + clazz + " has not been loaded yet.");
//		Checker.checkTrue(timelimit > 0, "The timelimit: " + timelimit + " should > 0.");
//		//init the explorer
//		this.explorer = new ForwardGenerator(model, (int)(timelimit * 1000), inputlimit, componentMgr, null, listenerMgr, outputTestFilters);
//		this.explorer.executionVisitor.visitors.addAll(visitors);
//		
//		//reset the randomize
//		Randomness.reset(System.currentTimeMillis());
//		
//		//random gen sequences
//		this.explorer.explore();
//		
//		List<ExecutableSequence> results = this.cleanResults(clazz);
//		//pick up one randomly
//		ExecutableSequence eSeq = Randomness.randomSetMember(results);
//		
//		return eSeq.sequence;
		List<ExecutableSequence> all = this.createAllSequences(clazz, timelimit);
		return Randomness.randomSetMember(all);
	}
	
	List<ExecutableSequence> remainValidSequences() {
		List<ExecutableSequence> sequences = new ArrayList<ExecutableSequence>();
	    for (ExecutableSequence p : explorer.outSeqs) {
	      sequences.add(p);
	    }
	    //Remove any sequences that throw randoop.util.ReflectionExecutor.TimeoutExceeded.
	    sequences = this.removeTimeoutExceptionSequence(sequences);
	    // While we're at it, remove the useless sequence "new Object()".
	    sequences = this.removeObjectAndRedundantSequence(sequences);
	    //remove sequence that has an exception check
		sequences = this.removeExceptionalSequence(sequences);
		
		return sequences;
	}
	
	private List<ExecutableSequence> cleanResults(Class<?> targetClass) {
		//output the sequences
		List<ExecutableSequence> sequences = new ArrayList<ExecutableSequence>();
	    for (ExecutableSequence p : explorer.outSeqs) {
	      sequences.add(p);
	    }
	    
	    // Remove any sequences that throw
	    // randoop.util.ReflectionExecutor.TimeoutExceeded.
	    sequences = this.removeTimeoutExceptionSequence(sequences);
	    
	    //remove sequence unrelevant to the target class 
	    sequences = this.removeUnrelevantSequence(targetClass, sequences);
	    
	    // While we're at it, remove the useless sequence "new Object()".
	    sequences = this.removeObjectAndRedundantSequence(sequences);
	    
	    //remove sequence that has an exception check
		sequences = this.removeExceptionalSequence(sequences);
		
		return sequences;
	}
	
	private List<ExecutableSequence> removeExceptionalSequence(List<ExecutableSequence> sequences) {
		List<ExecutableSequence> non_exception_list = new ArrayList<ExecutableSequence>();
	    for (ExecutableSequence es : sequences) {
	      if(!es.throwsException()) {
	    	non_exception_list.add(es);
	      }
	    }
	    return non_exception_list;
	}
	
	private List<ExecutableSequence> removeTimeoutExceptionSequence(
			List<ExecutableSequence> sequences) {
		List<ExecutableSequence> non_timeout_seqs = new ArrayList<ExecutableSequence>();
		boolean keep = true;
		for (ExecutableSequence es : sequences) {
			for (int i = 0; i < es.sequence.size(); i++) {
				List<Check> exObs = es.getChecks(i,
						ExpectedExceptionCheck.class);
				if (!exObs.isEmpty()) {
					assert exObs.size() == 1 : toString();
					ExpectedExceptionCheck eec = (ExpectedExceptionCheck) exObs
							.get(0);
					// Some TimeoutExceeded exceptions seem to be slipping
					// through. -MDE
					// System.out.println("ExpectedExceptionCheck: " +
					// eec.get_value());
					if (eec.get_value().equals(
							"randoop.util.ReflectionExecutor.TimeoutExceeded")) {
						keep = false;
						break;
					}
				}
			}
			if (keep)
				non_timeout_seqs.add(es);
		}
		return non_timeout_seqs;
	}
	
	private List<ExecutableSequence> removeUnrelevantSequence(Class<?> targetClass, List<ExecutableSequence> sequences) {
		List<ExecutableSequence> tc_seqs = new ArrayList<ExecutableSequence>();
	    for (ExecutableSequence es : sequences) {
	      boolean keep = false;
	      for (Variable v : es.sequence.getAllVariables()) {
	        if (targetClass.getName().equals(v.getType().getName())) {
	          keep = true;
	          break;
	        }
	      }
	      if (keep)
	        tc_seqs.add (es);
	    }
	    return tc_seqs;
	}

	private List<ExecutableSequence> removeObjectAndRedundantSequence(List<ExecutableSequence> sequences) {
		Sequence newObj = new Sequence().extend(objectConstructor);
		List<ExecutableSequence> retList = sequences;
	    if (GenInputsAbstract.remove_subsequences) {
	      List<ExecutableSequence> unique_seqs  = new ArrayList<ExecutableSequence>();
	      Set<Sequence> subsumed_seqs = explorer.subsumed_sequences();
	      for (ExecutableSequence es : sequences) {
	        if (!subsumed_seqs.contains(es.sequence) && !es.sequence.equals(newObj)) {
	          unique_seqs.add(es);
	        }
	      }
	      if (SketchTestOptions.verbose) {
	        System.out.printf("%d subsumed tests removed%n", sequences.size() - unique_seqs.size());
	      }
	      retList = unique_seqs;
	    }
	    return retList;
	}
}
